using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.CodeDom.Compiler;
using Microsoft.CSharp;

namespace TemplateCompiler {
	public class Compiler {
		public const string OPEN_TAG = "<%";
		public const string CLOSE_TAG = "%>";
		
		private List<Line> lines = new List<Line>();
		private List<Line> usings = new List<Line>();
		
		private bool compiled = false;
		
		private string file;
		
		private Assembly assembly;

		
		public void Compile(string fileToCompile, string[] referencedAssemblies) {
			int index = 0;
			if(fileToCompile.Contains('/')) {
				index = fileToCompile.LastIndexOf('/');
			}
			int length = fileToCompile.Length - index;
			if(fileToCompile.Contains('.')) {
				int period = fileToCompile.IndexOf('.', index);
				if(period >= 0) {
					length = period - index;
				}
			}			
			string className = fileToCompile.Substring(index+1, length-1);
			string urlPath = "/";
			if(index > 0) {
				int first = fileToCompile.IndexOf('/');
				if(first < index) {
					urlPath = fileToCompile.Substring(first, index-first);
				}
			}
			
			
			Compile(File.OpenRead(fileToCompile), className, urlPath, referencedAssemblies);
		}
		
		public void Compile(Stream stream, string className, string urlPath, string[] referencedAssemblies) {
			Compile(new StreamReader(stream), className, urlPath, referencedAssemblies);
		}
		
		private void Compile(StreamReader reader, string className, string urlPath, string[] referencedAssemblies) {
			ParseHeader(reader.ReadLine());
			
			StringBuilder content = new StringBuilder();
			string line;
			while((line = reader.ReadLine()) != null) {
				content.AppendLine(line);
			}
			
			ProcessContent(content.ToString());
			file = SaveContentToFile(className, urlPath);
			assembly = CompileFile(file, referencedAssemblies);
			compiled = true;
		}
		
		private string SaveContentToFile(string className, string urlPath) {
			string fileName = "views"+urlPath +"/" + className + ".cs";
			string url = urlPath + "/" + className +".milk";
			if(url.StartsWith("//")) {
				url = url.Substring(1);
			}
			StreamWriter writer = new StreamWriter(File.Create(fileName));
			writer.WriteLine("using System;");
			writer.WriteLine("using System.Linq;");
			writer.WriteLine("using System.Collections.Generic;");
			writer.WriteLine("using Servlets.Attributes;");
			writer.WriteLine("using Servlets.Api;");
			foreach(Line line in usings) {
				writer.WriteLine(line);
			}
			writer.WriteLine();
			writer.WriteLine("namespace Servlets.Milk {");
			writer.WriteLine("  [Servlet]");
			writer.WriteLine("  public class "+className+" {");
			writer.WriteLine("    [GetMethod(\"" +url +"\")]");
			writer.WriteLine("    public void Get"+className+"(IRequest request, IResponse response) {");
			writer.WriteLine("      ISession session = request.Session;");
			writer.WriteLine("      ResponseStream outstream = response.OutputStream;");
			writer.WriteLine("      IBody body = request.Body;");
			writer.WriteLine("      Cookies cookies = request.Cookies;");
			
			foreach(Line line in lines) {
				writer.WriteLine("      "+line.Line.TrimStart());
			}
			writer.WriteLine("    }");
			writer.WriteLine("  }");
			writer.WriteLine("}");
			
			writer.Flush();
			writer.Close();
			
			return fileName;
		}
		
		CompilerResults compilerResults;
		private Assembly CompileFile(string file, string[] referencedAssemblies) {
			CSharpCodeProvider codeProvider = new CSharpCodeProvider();
			
			string assemblyName = GetOutputtedAssemblyFileName();
			CompilerParameters parameters = new CompilerParameters(new string[]{"ServletApi.dll"}, assemblyName);
			foreach(string param in referencedAssemblies) {
				parameters.ReferencedAssemblies.Add(param);	
			}
			compilerResults = codeProvider.CompileAssemblyFromFile(parameters, file);
			return compilerResults.CompiledAssembly;
		}
		
		public string OutputtedFile() {
			CheckCompiled();
			return "views/"+file+".cs";
		}
		
		public string OutputtedAssemblyFile() {
			CheckCompiled();
			return GetOutputtedAssemblyFileName();
		}
		
		private string GetOutputtedAssemblyFileName() {
			return file+".dll";
		}
		
		public Assembly OutputtedAssembly() {
			CheckCompiled();
			
			return assembly;
		}
		
		public CompilerResults GetCompilerResults() {
			return compilerResults;
		}
		
		private void CheckCompiled() {
			if(!compiled) {
				throw new Exception("You cannot get the outputted file without first compiling. Call the Compile method first");
			}
		}
		
		private void ProcessContent(string content) {
			int index = 0;
			while(index >= 0) {
				int openIndex = content.IndexOf(OPEN_TAG, index);
				
				if(openIndex < 0) {
					break;
				}
				int length = openIndex - index;
				string html = content.Substring(index, length);
				AddHtmlLines(html);
				
				int closeIndex = content.IndexOf(CLOSE_TAG, openIndex + OPEN_TAG.Length);
				if(closeIndex < 0) {
					break;
				}
				length = closeIndex - openIndex;
				string code = content.Substring(openIndex+2, length-2);
				if(code.StartsWith("=")) {
					AddSugarCodeLines(code.Substring(1));
				} else if(code.StartsWith("@")) {
					AddAttributeLine(code.Substring(1));
				} else {
					AddCodeLines(code);
				}
				
				index = closeIndex + CLOSE_TAG.Length;
			}
			
			if(index < content.Length) {
				string html = content.Substring(index);
				AddHtmlLines(html);
			}
			
		}
		
		private void AddHtmlLines(string line) {
			line = line.Replace("\n", "\\n\n");
			string[] allLines = line.Split(new char[]{'\n'}, StringSplitOptions.RemoveEmptyEntries);
			foreach(string l in allLines) {
				lines.Add(new HtmlLine(l));
			}
		}
		
		private void AddCodeLines(string line) {
			//line = line.Replace("\n", "\\n\n");
			string[] allLines = line.Split(new char[]{'\n'}, StringSplitOptions.RemoveEmptyEntries);
			foreach(string l in allLines) {
				lines.Add(new CodeLine(l));
			}
		}
		
		private void AddSugarCodeLines(string line) {
			line = line.Replace("\n", "\\n\n");
			string[] allLines = line.Split(new char[]{'\n'}, StringSplitOptions.RemoveEmptyEntries);
			foreach(string l in allLines) {
				lines.Add(new SugarCodeLine(l));
			}
		}
		
		private void AddAttributeLine(string line) {
			if(line.Matches("using\\s*namespace=\"([\\w\\.]*)\"")) {
				usings.Add(new UsingLine(line));
			}
		}
		
		private void ParseHeader(string header) {
			bool matches = header.StartsWith(OPEN_TAG + "@ MILK") && header.EndsWith(CLOSE_TAG);
			
			if(!matches) {
				throw new InvalidHeaderException(string.Format("The first line must be '{0}@ MILK {1}'", OPEN_TAG, CLOSE_TAG));
			}
		}
	}
	
	interface Line {
		string Line{get;set;}
	}
	
	class HtmlLine : Line {
		private string _line;
		public string Line{
			get {
				return _line;
			}
			set {
				this._line = string.Format("outstream.Write(\"{0}\");",
				                           value.Replace("\"", "\\\"")
				                           		.Replace("\n","\\n")
				                           		.Replace("\r","\\r"));
			}
		}
		
		public HtmlLine(string line) {
			this.Line = line;
		}
		
		public override string ToString ()
		{
			return Line;
		}
	}
	
	class UsingLine : Line {
		public string Line{get;set;}
		private const String pattern = "using\\s*namespace=[\"']([\\w\\.]*)[\"']";
		
		public UsingLine(string line) {
			Regex regex = new Regex(pattern);
			Match match = regex.Match(line);
			if(match.Success) {
				bool first = true;
				foreach(Group g in match.Groups) {
					if(first) {
						first = false;
						continue;
					}
					this.Line = string.Format("using {0};", g.Value);
				}
			}
		}
		
		public static bool IsUsingLine(String line) {
			Regex regex = new Regex(pattern);
			Match match = regex.Match(line);
			return match.Success;
		}
		
		public override string ToString ()
		{
			return Line;
		}
	}
	
	class TagLine : Line {
		public string Line{get;set;}
		private const String pattern = "tag\\s*name=[\"']([\\w\\.]*)[\"']\\s*classname=[\"']([\\w\\.]*)[\"']";
		
		public TagLine(string line) {
			Regex regex = new Regex(pattern);
			Match match = regex.Match(line);
			if(match.Success) {
				String name = match.Groups[1].Value;
				String className = match.Groups[2].Value;
				this.Line = string.Format("tags[\"{0}\"] = \"{1}\";", name, className);
			}
		}
		
		public static bool IsTagLine(String line) {
			Regex regex = new Regex(pattern);
			Match match = regex.Match(line);
			return match.Success;
		}
		
		public override string ToString ()
		{
			return Line;
		}
	}
	
	class CodeLine : Line {
		public string Line{get;set;}
		
		public CodeLine(string line) {
			this.Line = line;
		}
		
		public override string ToString ()
		{
			return Line;
		}
	}
	
	class SugarCodeLine : Line {
		private string _line;
		public string Line{
			get {
				return _line;
			}
			set {
				this._line = string.Format("outstream.Write({0});", value);
			}
		}
		
		public SugarCodeLine(string line) {
			this.Line = line;
		}
		
		public override string ToString ()
		{
			return Line;
		}
	}
}
